package com.atguigu.seckill.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.to.mq.SeckillOrderTo;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberResponseVo;
import com.atguigu.seckill.feign.CouponFeignService;
import com.atguigu.seckill.feign.ProductFeignService;
import com.atguigu.seckill.interceptor.LoginInterceptor;
import com.atguigu.seckill.service.SeckillService;
import com.atguigu.seckill.to.SeckillSkuRedisTo;
import com.atguigu.seckill.vo.SeckillSessionWithSkusVo;
import com.atguigu.seckill.vo.SeckillSkuVo;
import com.atguigu.seckill.vo.SkuInfoVo;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @Author ShiJieLin
 * @Date 2022/5/3 16:40
 * @Version 1.0
 */
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {

    @Autowired
    private CouponFeignService couponFeignService;
    @Autowired
    private ProductFeignService productFeignService;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 每场活动的开始和结束时间作为key
     */
    private final String SESSION__CACHE_PREFIX = "seckill:sessions:";

    /**
     * 每个场次关联的商品信息
     */
    private final String SECKILL_CHARE_PREFIX = "seckill:skus";

    /**
     * 请求后面加上随机码，防止脚本抢秒杀
     */
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";

    @Override
    public void uploadSeckillSkuLatest3Days() {
        // 1.获取到最近三天需要秒杀的场次信息
        R daySession = couponFeignService.getLates3DaySession();

        if (daySession.getCode() == 0) {

            List<SeckillSessionWithSkusVo> sessionData = daySession.getData(new TypeReference<List<SeckillSessionWithSkusVo>>() {
            });
            // 缓存到Redis
            // 1.缓存活动信息
            saveSessionInfos(sessionData);

            // 2.缓存活动的关联商品信息
            saveSessionSkuInfo(sessionData);
        }
    }

    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        // 1.获取当前时间
        long currentTime = System.currentTimeMillis();
        // 2.从redis中获取所有的秒杀活动的 开始和结束时间 1521387877600751617-1515872650004156418
        Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*");

        for (String key : keys) {
            // 3.获取每场秒杀活动的开始和结束时间
            String replace = key.replace(SESSION__CACHE_PREFIX, "");
            String[] split = replace.split("_");
            long startTime = Long.parseLong(split[0]);
            long endTime = Long.parseLong(split[1]);
            // 4.判断当前的时间是否属于该场秒杀活动
            if (currentTime >= startTime && currentTime <= endTime) {
                // 4.1取出当前活动的所有商品信息
                List<String> range = redisTemplate.opsForList().range(key, -100, 100);
                BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                List<String> list = operations.multiGet(range);
                // 4.2 将符合的商品信息进行返回
                if (null != list) {
                    List<SeckillSkuRedisTo> redisToStream = list.stream().map((item) -> {
                        SeckillSkuRedisTo redisTo = JSON.parseObject(item, SeckillSkuRedisTo.class);
                        return redisTo;
                    }).collect(Collectors.toList());
                    return redisToStream;
                }
            }
        }
        return null;
    }

    @Override
    public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) {

        BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        // 1.获取所有存储商品信息的键
        Set<String> keys = operations.keys();
        // 2.查询指定skuId商品的秒杀活动信息
        if (null != keys && keys.size() > 0) {
            // 2.2
            for (String key : keys) {
                String[] split = key.split("-");
                Long redisSkuId = Long.parseLong(split[1]);
                // 2.2 如果匹配上，那么就取出当前的商品信息
                if (skuId.equals(redisSkuId)) {
                    // 2.3 取出数据，进行封装
                    String s = operations.get(key);
                    SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);
                    // 2.4判断当前秒杀商品是否已经开始，如果开始才将随机码进行返回
                    Long currentTime = System.currentTimeMillis();
                    Long startTime = seckillSkuRedisTo.getStartTime();
                    Long endTime = seckillSkuRedisTo.getEndTime();
                    // 2.4.1 符合时间
                    if (currentTime >= startTime && currentTime <= endTime) {
                        return seckillSkuRedisTo;
                    }
                    seckillSkuRedisTo.setRandomCode(null);
                    return seckillSkuRedisTo;
                }
            }
        }
        return null;
    }

    @Override
    public String kill(String killId, String key, Integer num) {
        // 1.获取当前时间
        long s1 = System.currentTimeMillis();
        // 2.获取用户登陆信息
        MemberResponseVo memberVo = LoginInterceptor.loginUser.get();
        // 3.获取当前商品的秒杀信息
        BoundHashOperations<String, String, String> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String jsonSkuInfo = operations.get(killId);
        if (StringUtils.isEmpty(jsonSkuInfo)) {
            return null;
        }
        // 4.校验合法性
        SeckillSkuRedisTo redisTo = JSON.parseObject(jsonSkuInfo, SeckillSkuRedisTo.class);
        Long startTime = redisTo.getStartTime();
        Long endTime = redisTo.getEndTime();
        long currentTimeMillis = System.currentTimeMillis();
        // 4.1判断是否是合法时间
        if (currentTimeMillis >= startTime && currentTimeMillis <= endTime) {
            // 4.2效验随机码和商品id
            String randomCode = redisTo.getRandomCode();
            String skuIdAndSessionId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId();
            // 4.3比对随机码和场次SkuId是否一致
            if (randomCode.equals(key) && skuIdAndSessionId.equals(killId)) {
                // 4.3用户秒杀购买的商品限制购买的数量
                Integer seckillLimit = redisTo.getSeckillLimit();
                // 4.4上架使用的随机值作为 信号量 存储的是 商品的库存
                String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode);
                int productNum = Integer.parseInt(seckillCount);
                // 4.5库存得大于0，并且用户购买的数量小于限购数量，并且 库存得大于用户购买得数量
                if (productNum > 0 && num <= seckillLimit && productNum > num) {
                    // 4.6保证用户只能购买一次，接口得幂等性
                    String redisKey = memberVo.getId() + "-" + skuIdAndSessionId;
                    // 4.7活动过期时间
                    Long ttl = endTime - currentTimeMillis;
                    // setIfAbsent只能保存 一次，之后返回false
                    Boolean isFlag = redisTemplate.opsForValue().setIfAbsent(redisKey,
                            num.toString(), ttl, TimeUnit.MILLISECONDS);
                    if (isFlag) {
                        // 占位成功说明从来没有买过,分布式锁(获取信号量-1)
                        RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                        // 尝试获取，如果获取成功则代表着秒杀成功
                        boolean tryAcquire = semaphore.tryAcquire(num);
                        if (tryAcquire) {
                            // 4.8创建订单号和订单信息发送给MQ
                            String timeId = IdWorker.getTimeId();
                            SeckillOrderTo orderTo = new SeckillOrderTo();
                            orderTo.setOrderSn(timeId);
                            orderTo.setMemberId(memberVo.getId());
                            orderTo.setNum(num);
                            orderTo.setPromotionSessionId(redisTo.getPromotionSessionId());
                            orderTo.setSkuId(redisTo.getSkuId());
                            orderTo.setSeckillPrice(redisTo.getSeckillPrice());
                            // 4.9秒杀成功，将订单信息发送到MQ中，交给订单服务进行慢慢创建
                            rabbitTemplate.convertAndSend("order.event.exchange", "order.seckill.routingKey",
                                    orderTo);
                            long s2 = System.currentTimeMillis();
                            log.info("耗时..." + (s2 - s1));
                            return timeId;
                        }
                    }
                }
            }
        }
        long s3 = System.currentTimeMillis();
        log.info("耗时..." + (s3 - s1));
        return null;
    }

    /**
     * 缓存每场活动的信息
     * 将每场活动的开始时间和结束时间作为Key，然后，将每场活动关联的
     * 商品信息SkuId作为value
     *
     * @param sessionData
     */
    private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessionData) {

        sessionData.stream().forEach(seckillSessionWithSkusVo -> {
            // 1.获取每场活动的开始和结束时间
            Long startTime = seckillSessionWithSkusVo.getStartTime().getTime();
            Long endTime = seckillSessionWithSkusVo.getEndTime().getTime();
            // 2.将每场活动的时间，作为key
            String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime;
            // 3.判断Redis中是否有该信息，如果没有才进行添加，避免重复提交
            Boolean hasKey = redisTemplate.hasKey(key);
            // 4.没有再添加
            if (!hasKey) {
                // 4.1获取到每场秒杀活动的ID和对应的SkuId
                List<SeckillSkuVo> relationSkus = seckillSessionWithSkusVo.getRelationSkus();

                List<String> PromotionSessionIdWithSkuId = relationSkus.stream().map((skuInfo) ->
                                skuInfo.getPromotionSessionId() + "-" + skuInfo.getSkuId())
                        .collect(Collectors.toList());
                redisTemplate.opsForList().leftPushAll(key, PromotionSessionIdWithSkuId);
            }
        });
    }

    /**
     * 缓存每场活动的商品信息
     *
     * @param sessionData
     */
    private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessionData) {

        sessionData.stream().forEach((session) -> {

            BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
            // 1.遍历每场活动信息
            session.getRelationSkus().stream().forEach((seckillSkuVo) -> {
                // 生成随机码
                String randomCode = UUID.randomUUID().toString().replace("-", "");
                // 获得活动ID和商品SkuId作为key
                String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString();
                // 判断是否重复
                if (!operations.hasKey(redisKey)) {
                    // 封装保存redis中的商品信息
                    SeckillSkuRedisTo skuRedisTo = new SeckillSkuRedisTo();
                    Long skuId = seckillSkuVo.getSkuId();
                    // 2.根据SkuId查询远程商品服务，获取商品的详细信息
                    R skuInfo = productFeignService.getSkuInfo(skuId);
                    if (skuInfo.getCode() == 0) {
                        // 商品信息
                        SkuInfoVo skuInfoData = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                        });
                        skuRedisTo.setSkuInfo(skuInfoData);
                    }
                    // 3.秒杀信息
                    BeanUtils.copyProperties(seckillSkuVo, skuRedisTo);
                    // 4.设置当前秒杀活动的时间信息
                    skuRedisTo.setStartTime(session.getStartTime().getTime());
                    skuRedisTo.setEndTime(session.getEndTime().getTime());
                    // 5.设置随机验证码
                    skuRedisTo.setRandomCode(randomCode);
                    // 将信息序列化，存入redis中
                    String seckillValue = JSON.toJSONString(skuRedisTo);

                    String key = (seckillSkuVo.getPromotionSessionId().toString()) + "-" + (seckillSkuVo.getSkuId().toString());

                    // 存入redis
                    operations.put(key, seckillValue);
                    //  如果当前这个场次的商品库存信息已经上架就不需要上架
                    // 6.使用库存作为分布式Redisson信号量（限流）
                    //     使用库存作为分布式信号量
                    RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode);
                    // 商品可以秒杀的数量作为信号量
                    semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
                }
            });
        });
    }
}
